package de.preamp.clients.java.player;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import de.preamp.clients.java.data.Track;
import de.preamp.clients.java.playlist.Playlist;

/**
 * This class manages caching of the played playback. It caches the track
 * currently played and, if it is completely cached, the next track in the
 * playlist is precached.
 * 
 * @author André Stannek
 * @since 2010-08-21
 * @version 2011-06-05
 */
public class PlaylistCacher
{
    /**
     * A {@link Map} holding an InputStream for each {@link Track} in the
     * {@link Playlist}. The {@link TrackCacher} can be prefilled with audio
     * data.
     */
    private final transient Map<Track, TrackCacher> map;

    /**
     * The {@link TrackCacher} that is caching right now.
     */
    private transient TrackCacher cachingCache;

    /**
     * The playlist to cache.
     */
    private final transient Playlist playlist;

    /**
     * Constructor.
     * 
     * @param playlist
     *            the playlist that is played by this instance
     */
    public PlaylistCacher(final Playlist playlist)
    {
	super();
	this.playlist = playlist;
	map = new HashMap<Track, TrackCacher>();
	for (final Track track : playlist)
	{
	    map.put(track, new TrackCacher(track));
	}
    }

    /**
     * Returns a InputStream for a Track and starts caching it.
     * 
     * @param track
     *            the track
     * @return the input stream for the track
     */
    public final InputStream getStream(final Track track)
    {
	if (cachingCache != null)
	{
	    cachingCache.pauseCaching();
	}
	final TrackCacher cache = map.get(track);
	cache.startCaching();
	cachingCache = cache;
	// wait because the caching thread needs time to build the streams
	while (!cache.isAlive())
	{
	    try
	    {
		Thread.sleep(10);
	    }
	    catch (final InterruptedException e)
	    {
		return cache.getStream();
	    }
	}
	return cache.getStream();
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#finalize()
     */
    @Override
    protected final void finalize() throws Throwable // NOPMD
    {
	for (final TrackCacher cache : map.values())
	{
	    cache.stopCaching();
	}
	super.finalize();
    }

    /**
     * Used by inner class {@link TrackCacher} to notify that caching of a track
     * is done.
     * 
     * @param track
     *            the track which caching is done.
     */
    private void cachingDone(final Track track)
    {
	final Iterator<Track> iterator = playlist.iterator();
	while (iterator.hasNext())
	{
	    if (iterator.next().equals(track))
	    {
		getStream(iterator.next());
		break;
	    }
	}
    }

    /**
     * A class managing the caching of an InputStream into another InputStream
     * using piped streams.
     * 
     * @author André Stannek
     * @since 2010-08-21
     * @version 2010-08-30
     */
    private class TrackCacher extends Thread
    {
	/**
	 * The InputStream that is the actual data source.
	 */
	private transient InputStream input;

	/**
	 * the InputStream that contains the cached data.
	 */
	private final transient PipedInputStream cachedInput;

	/**
	 * Flag indicating if the complete Stream is cached.
	 */
	private transient boolean cached;

	/**
	 * Flag indicatin if caching is interupted.
	 */
	private transient boolean interrupted;

	/**
	 * Flag indicating if caching is paused.
	 */
	private transient boolean paused;

	/**
         * 
         */
	private final transient Track track;

	/**
	 * @param track
	 *            the track cached by this stream
	 */
	public TrackCacher(final Track track)
	{
	    super();
	    this.track = track;
	    cached = false;
	    interrupted = false;
	    paused = false;
	    cachedInput = new PipedInputStream();
	    try
	    {
		input = track.getURL().openConnection().getInputStream();
	    }
	    catch (final IOException e)
	    {
		input = null; // NOPMD
	    }
	}

	/**
	 * Pauses the caching procedure. It can be resumed any time using
	 * {@code startCaching()}.
	 */
	public void pauseCaching()
	{
	    paused = true;
	}

	/**
	 * Begins the caching procedure or resumes it if paused. If the Track is
	 * already completely cached it does nothing.
	 */
	public void startCaching()
	{
	    if (!cached && !isAlive())
	    {
		start();
	    }
	    else if (!cached && isAlive())
	    {
		paused = false;
	    }
	}

	/**
	 * Sets the interrupt flag so that the caching is stopped.
	 */
	public void stopCaching()
	{
	    interrupted = true;
	}

	/**
	 * run() should not be called directly. Use startCaching() instead.
	 */
	@Override
	public void run()
	{
	    try
	    {
		final PipedOutputStream outputPipe =
			new PipedOutputStream(cachedInput);
		int readByte = input.read();
		while (readByte != -1)
		{
		    outputPipe.write(readByte);
		    readByte = input.read();
		    if (interrupted)
		    {
			outputPipe.flush();
			outputPipe.close();
			input.close();
			cachedInput.close();
			cached = false;
			return;
		    }
		    while (paused)
		    {
			try
			{
			    Thread.sleep(1000);
			}
			catch (final InterruptedException exception)
			{
			    continue;
			}
		    }
		}
		cached = true;
		cachingDone(track);
	    }
	    catch (final IOException exception)
	    {
		Logger.getAnonymousLogger().log(Level.WARNING,
			"A track couldn't be cached", exception);
	    }
	}

	/**
	 * Returns the InputStream that contains the cached data. If the caching
	 * procedure is not complete, startCaching() is invoked.
	 * 
	 * @return the cached input stream
	 */
	public InputStream getStream()
	{
	    if (!cached)
	    {
		startCaching();
	    }
	    return new BufferedInputStream(cachedInput);
	}

    }
}
